package com.simafei.flow.web.service.impl;

import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.simafei.flow.core.common.Variable;
import com.simafei.flow.core.data.DataManager;
import com.simafei.flow.core.data.LoadSpec;
import com.simafei.flow.web.common.PageUtils;
import com.simafei.flow.web.common.error.BizErrorCode;
import com.simafei.flow.web.common.error.BizException;
import com.simafei.flow.web.dao.DataSourceMapper;
import com.simafei.flow.web.domain.req.DataSourceReq;
import com.simafei.flow.web.domain.req.LoadExecReq;
import com.simafei.flow.web.domain.req.PageParam;
import com.simafei.flow.web.domain.resp.DataSourceResp;
import com.simafei.flow.web.domain.resp.PageResult;
import com.simafei.flow.web.domain.resp.VariableResp;
import com.simafei.flow.web.entity.DataSourcePO;
import com.simafei.flow.web.service.IDataSourceService;
import com.simafei.flow.web.transfer.DataSourceTransfer;
import com.simafei.flow.web.transfer.VariableTransfer;
import com.zaxxer.hikari.HikariDataSource;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author fengpengju
 * @since 2024-07-15
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class DataSourceServiceImpl extends ServiceImpl<DataSourceMapper, DataSourcePO> implements IDataSourceService {

    private final DataManager dataManager;

    @PostConstruct
    public void init() {
        List<DataSourcePO> list = list();
        list.forEach(po -> addToManager(po.getDsName(), po));
    }

    @Override
    public PageResult<DataSourceResp> listPage(PageParam param) {
        Page<DataSourcePO> page = page(new Page<>(param.getPageNo(), param.getPageSize()));
        return PageUtils.buildPageResult(page, DataSourceTransfer.INSTANCE::toResp);
    }

    @Override
    public List<String> tables(String dataSource, String tableName) {
        checkAddedToManager(dataSource, getOne(dataSource));
        List<String> tables = dataManager.tables(dataSource);
        if (StrUtil.isEmpty(tableName)) {
            return tables;
        } else {
            return tables.stream().filter(t -> t.contains(tableName)).toList();
        }
    }

    @Override
    public List<VariableResp> columns(String dataSource, String tableName) {
        DataSourcePO one = getOne(dataSource);
        checkAddedToManager(dataSource, one);
        List<Variable> columns = dataManager.columns(dataSource, one.getDbName(), tableName);
        return VariableTransfer.INSTANCE.boToResp(columns);
    }

    @Override
    public Long add(DataSourceReq dataSourceReq) {
        DataSourcePO po = DataSourceTransfer.INSTANCE.toPo(dataSourceReq);
        po.setDbName(extractDbNameFromUrl(po.getUrl()));

        boolean exists = exists(new LambdaQueryWrapper<DataSourcePO>().eq(DataSourcePO::getDsName, po.getDsName()));
        testConnectivity(po);
        if (exists) {
            throw new BizException(BizErrorCode.DUPLICATE_KEY_ERROR, po.getDsName());
        }
        if (save(po)) {
            return po.getId();
        }
        return null;
    }

    @Override
    public boolean delete(Long id) {
        DataSourcePO po = getById(id);
        dataManager.remove(po.getDsName());
        return removeById(id);
    }

    @Override
    public List<Map<String, Object>> execute(LoadExecReq req) {
        LoadSpec spec = LoadSpec.builder().dataSource(req.getDataSource())
                .tableName(req.getTableName())
                .columns(req.getColumns())
                .criteria(req.getCriteria())
                .build();
        return dataManager.load(spec, req.getParams());
    }

    private void checkAddedToManager(String dsName, DataSourcePO po) {
        if (!dataManager.exists(dsName)) {
            addToManager(dsName, po);
        }
    }

    private DataSourcePO getOne(String dsName) {
        DataSourcePO one = getOne(new LambdaQueryWrapper<DataSourcePO>().eq(DataSourcePO::getDsName, dsName));
        if (one == null) {
            throw new BizException(BizErrorCode.DATA_SOURCE_NOT_DEFINED, dsName);
        }
        return one;
    }

    private void addToManager(String dsName, DataSourcePO one) {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(one.getUrl());
        dataSource.setUsername(one.getUsername());
        dataSource.setPassword(one.getPassword());
        dataSource.setDriverClassName(one.getDriverClass());

        dataManager.add(dsName, dataSource);
    }

    private void testConnectivity(DataSourcePO req) {
        try {
            Class.forName(req.getDriverClass());

            try (Connection conn = DriverManager
                    .getConnection(req.getUrl(), req.getUsername(), req.getPassword())) {
                if (conn == null) {
                    throw new BizException(BizErrorCode.DATA_SOURCE_CONNECT_FAILURE, req.getDsName());
                }
            }
        } catch (SQLException e) {
            // 这里不需要打印详细异常
            log.warn("test connectivity failed, e={}", e.getMessage());
            throw new BizException(BizErrorCode.DATA_SOURCE_CONNECT_FAILURE, e.getMessage());
        } catch (ClassNotFoundException e) {
            log.error("test connectivity failed, e={}", e.getMessage());
            throw new BizException(BizErrorCode.DRIVER_CLASS_NOT_FOUND, req.getDriverClass());
        }
    }

    private String extractDbNameFromUrl(String url) {
        int startIndex = url.indexOf("//") + 2;
        int endIndex = url.indexOf("/", startIndex);
        // 如果没有找到"/"，证明没有数据库名
        if (endIndex == -1) {
            throw new BizException(BizErrorCode.DATABASE_EMPTY);
        }
        String path = url.substring(endIndex + 1);

        // 如果路径后面有查询字符串，去除查询字符串
        int queryIndex = path.indexOf("?");
        if (queryIndex != -1) {
            path = path.substring(0, queryIndex);
        }
        if (StrUtil.isEmpty(path)) {
            throw new BizException(BizErrorCode.DATABASE_EMPTY);
        }

        return path;

    }
}
